Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce CardEditUIHandler for the CardEditUI #10462

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

toluo-stripe
Copy link
Contributor

@toluo-stripe toluo-stripe commented Mar 21, 2025

Summary

This will hold all the state for the content on the CardDetails UI. It will also emit changes to CardUpdateParams to the UpdatePaymentMethodInteractor.

CardUpdateParams is the model that holds all the properties that have changed.

It only supports CBC changes at the moment.

Untitled diagram-2025-03-14-151415

Motivation

Breaking up this PR
JIRA

https://docs.google.com/document/d/1mvmApRTfsmQd0A4-b6g8EGuerlF0VE6N2eHr1F19HSk/edit?tab=t.0#heading=h.gl2i202sfdwi

Testing

  • Added tests
  • Modified tests
  • Manually verified

Changelog

Copy link
Contributor

github-actions bot commented Mar 21, 2025

Diffuse output:

OLD: identity-example-release-base.apk (signature: V1, V2)
NEW: identity-example-release-pr.apk (signature: V1, V2)

          │          compressed           │         uncompressed         
          ├───────────┬───────────┬───────┼───────────┬───────────┬──────
 APK      │ old       │ new       │ diff  │ old       │ new       │ diff 
──────────┼───────────┼───────────┼───────┼───────────┼───────────┼──────
      dex │     2 MiB │     2 MiB │   0 B │   4.1 MiB │   4.1 MiB │  0 B 
     arsc │     1 MiB │     1 MiB │   0 B │     1 MiB │     1 MiB │  0 B 
 manifest │   2.3 KiB │   2.3 KiB │   0 B │     8 KiB │     8 KiB │  0 B 
      res │ 302.9 KiB │ 302.9 KiB │   0 B │   457 KiB │   457 KiB │  0 B 
   native │   6.2 MiB │   6.2 MiB │   0 B │  15.8 MiB │  15.8 MiB │  0 B 
    asset │   7.3 KiB │   7.3 KiB │   0 B │   7.1 KiB │   7.1 KiB │  0 B 
    other │  95.1 KiB │  95.2 KiB │ +12 B │ 182.4 KiB │ 182.4 KiB │  0 B 
──────────┼───────────┼───────────┼───────┼───────────┼───────────┼──────
    total │   9.6 MiB │   9.6 MiB │ +12 B │  21.6 MiB │  21.6 MiB │  0 B 

 DEX     │ old   │ new   │ diff      
─────────┼───────┼───────┼───────────
   files │     1 │     1 │ 0         
 strings │ 20142 │ 20142 │ 0 (+0 -0) 
   types │  6235 │  6235 │ 0 (+0 -0) 
 classes │  5026 │  5026 │ 0 (+0 -0) 
 methods │ 30047 │ 30047 │ 0 (+0 -0) 
  fields │ 17385 │ 17385 │ 0 (+0 -0) 

 ARSC    │ old  │ new  │ diff 
─────────┼──────┼──────┼──────
 configs │  164 │  164 │  0   
 entries │ 3644 │ 3644 │  0
APK
    compressed    │   uncompressed   │                                           
──────────┬───────┼───────────┬──────┤                                           
 size     │ diff  │ size      │ diff │ path                                      
──────────┼───────┼───────────┼──────┼───────────────────────────────────────────
   29 KiB │  +8 B │    64 KiB │  0 B │ ∆ META-INF/CERT.SF                        
 25.7 KiB │  +4 B │    64 KiB │  0 B │ ∆ META-INF/MANIFEST.MF                    
    271 B │  -1 B │     120 B │  0 B │ ∆ META-INF/version-control-info.textproto 
  1.2 KiB │  +1 B │   1.2 KiB │  0 B │ ∆ META-INF/CERT.RSA                       
──────────┼───────┼───────────┼──────┼───────────────────────────────────────────
 56.1 KiB │ +12 B │ 129.4 KiB │  0 B │ (total)

@toluo-stripe toluo-stripe marked this pull request as ready for review March 21, 2025 20:15
@toluo-stripe toluo-stripe requested review from a team as code owners March 21, 2025 20:15
Copy link
Collaborator

@amk-stripe amk-stripe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should merge this in once it's actually being used, rather than checking in unused code. I think a natural way to merge in this initial change would be to just replace the existing card details UI with this new UI and its handler. So I would also expect that all the card brand choice changes are included in the initial PR, but probably not the "card details" parts (presumably those are the other fields that will be changed as part of the edit card project)

@toluo-stripe toluo-stripe force-pushed the tolu/uspm/card_edit_handler branch from 5a49c4e to 918bf9a Compare March 24, 2025 16:19
@toluo-stripe toluo-stripe force-pushed the tolu/uspm/card_edit_handler branch from 4c0a385 to f1389e5 Compare March 31, 2025 18:39
*
* @param cardBrandChoice The newly selected card brand choice
*/
fun onBrandChoiceChanged(cardBrandChoice: CardBrandChoice)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should replace the callbacks in favor of a single method that handles view actions. A sealed class representing all the view actions can then just be appended to.

    sealed interface ViewAction {
        data class OnBrandChoiceChanged(val cardBrandChoice: CardBrandChoice) : ViewAction
        data class OnCardDetailsChanged(val cardUpdateParams: CardUpdateParams?) : ViewAction
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • fun onBrandChoiceChanged(cardBrandChoice: CardBrandChoice) is a function meant to be called from the UI when a new brand is selected.
  • val onBrandChoiceChanged: BrandChoiceCallback is meant for analytics purposes. It's fired when the selected brand in the function above is different from the original. The brandChoiceChange function parameter is sent downstream from [Payment|Customer]SheetViewModel to UpdatePaymentMethodInteractor. EditCardDetailsInteractor is handling CBC change now, that's why I added the function parameter
    *val onCardDetailsChanged: CardDetailsCallback is a callback that emits edit card parameters to a consumer (UpdatePaymentMethodInteractor)

I don't think onCardDetailsChanged fits into the viewAction because of the behaviour I described above. I could move the brandChanged function into a ViewAction interface. What do you think?

/**
* Callback for when the card brand choice changes.
*/
val onBrandChoiceChanged: BrandChoiceCallback
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this unintentional? I see two callbacks for handling changes to card brand choice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one highlighted in this comments is to send an analytics event upstream. The other is for when the user selects a brand from the dropdown. I expanded more here

I could rename it to something with analytics in its name. What do you think?


internal typealias CardDetailsCallback = (CardUpdateParams?) -> Unit

internal typealias BrandChoiceCallback = (CardBrand) -> Unit
Copy link
Collaborator

@samer-stripe samer-stripe Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels redundant since brand choice can be controlled from CardUpdateParams as well. Are we going to combine these later? Could each individual field be a view action?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This callback is for analytics. I don't want the component using this interactor to have to determine when the cardbrand is changed. CardUpdateParams has expiryDate and address values as well, so a new cardUpdateParam does not mean the card brand is changed. The consumer will need to add logic (trivial I admit) to determine if there's a new card brand.

internal data class CardUpdateParams(
    val expiryMonth: Int? = null,
    val expiryYear: Int? = null,
    val cardBrand: CardBrand? = null,
    val billingDetails: PaymentMethod.BillingDetails? = null,
)

}
)

assertThat(handler.selectedBrand).isEqualTo(CardBrand.CartesBancaires)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does it default to CartesBancaires?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CARD_WITH_NETWORKS test fixture defaults to CartesBancaires. It was probably an arbitrary choice when the fixture was created

private fun handler(
        card: PaymentMethod.Card = PaymentMethodFixtures.CARD_WITH_NETWORKS,
        cardBrandFilter: CardBrandFilter = DefaultCardBrandFilter,

Comment on lines 50 to 75
var cardUpdateParams: CardUpdateParams? = null
val handler = handler(
onCardDetailsChanged = {
cardUpdateParams = it
}
)

assertThat(handler.selectedBrand).isEqualTo(CardBrand.CartesBancaires)

handler.brandChanged(CardBrand.Visa)

assertThat(cardUpdateParams).isEqualTo(cardUpdateParams(cardBrand = CardBrand.Visa))

handler.brandChanged(CardBrand.CartesBancaires)

assertThat(cardUpdateParams).isNull()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usually tests follow a pattern of:

  • setup
  • test
  • assert

This is also known as: arrange, act, assert

in this case I suppose the test should look like this. Note that there is a single line between each section which acts as a separator between each section: arrange, act and assert.

@Test
   fun  xxx() {
        var cardUpdateParams: CardUpdateParams? = null
        val handler = handler(
            onCardDetailsChanged = {
                cardUpdateParams = it
            }
        )

        handler.brandChanged(CardBrand.Visa)

        assertThat(cardUpdateParams).isEqualTo(cardUpdateParams(cardBrand = CardBrand.Visa))
   }

If we want to test initial states like defaulting to CardBrand.CartesBancaires or cardUpdateParams being null, we should keep that in a different test that is ONLY testing that.

Comment on lines 18 to 52
/**
* The card being edited.
*/
val card: PaymentMethod.Card

/**
* Filter for determining which card brands are available.
*/
val cardBrandFilter: CardBrandFilter

/**
* Icon resource ID for the payment method.
*/
val paymentMethodIcon: Int

/**
* Whether to show the card brand dropdown.
*/
val showCardBrandDropdown: Boolean

/**
* Current state of the card edit UI.
*/
val state: StateFlow<State>

/**
* Callback for when the card brand choice changes.
*/
val onBrandChoiceChanged: BrandChoiceCallback

/**
* Callback for when card details change. It provides the values needed to
* update the card, if any.
*/
val onCardDetailsChanged: CardDetailsCallback
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imho, these comments hurt more than the help -- imho, i'd remove them because the variable names seem to speak for themselves, but i'll leave it up to you guys.

/**
* Whether to show the card brand dropdown.
*/
val showCardBrandDropdown: Boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho, shouldShowCardBrandDropdown or isCardBrandDropdownShown are better names as this is a Boolean


internal typealias CardDetailsCallback = (CardUpdateParams?) -> Unit

internal typealias BrandChoiceCallback = (CardBrand) -> Unit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/BrandChoiceCallback/CardBrandChoiceCallback/
or maybe
s/BrandChoiceCallback/CardBrandCallback/

import com.stripe.android.paymentsheet.CardUpdateParams
import kotlinx.coroutines.flow.StateFlow

internal typealias CardDetailsCallback = (CardUpdateParams?) -> Unit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling this "CardDetailsCallback" introduces "Details" as new nomenclature. It seems like we avoid this by just changing the name of this to: CardUpdateParamsCallback

onCardDetailsChanged: CardUpdateParamsCallback
): EditCardDetailsInteractor
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of the exposed attributes here are implementation heavy and we should remove what's necessary. Ideally the interactor only exposes what is important to the UI which is:

  • an observable state
  • a set of view actions

The rest of these attributes should be implementation detail specific to the default implementation of the interactor. These can be managed as private parameters on the default implementation:

  • The card being edited.
  • The card brand filter.
  • payment method icon
  • analytics callbacks for card details changing and card brand choice.

See the SelectSavedPaymentMethodsInteractor as an example. The default create method exposed via the interactor can also just create the default implementation.

)
}
}
}
Copy link
Collaborator

@samer-stripe samer-stripe Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the interactor within CardDetailsUI and UpdatePaymentMethodInteractor in this PR.

*
* @param cardBrandChoice The newly selected card brand choice
*/
fun onBrandChoiceChanged(cardBrandChoice: CardBrandChoice)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will be more view actions for handling other card edit changes? When we do add those actions, let's create a ViewAction type with all the possible actions that the interactor can handle then expose a single method called handleViewAction that the UI can use.

Doesn't need to happen in this PR!

Update EditCardDetailsInteractor.kt

Remove callback from interface

rename factory

Update UpdatePaymentMethodInteractor.kt

Update UpdatePaymentMethodInteractor.kt

Update FakeEditCardDetailsInteractor.kt
@toluo-stripe toluo-stripe force-pushed the tolu/uspm/card_edit_handler branch from 9a16640 to c2985ed Compare April 1, 2025 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants